﻿#region Using References

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;

#endregion

namespace Beaker.OpenCube.Mapping
{
	/// <summary>
	/// Factory for creating dictionary mappers
	/// </summary>
	public static class Mapper
	{
		/// <summary>
		/// Creates a deserializer for the given target type.
		/// </summary>
		/// <typeparam name="TTarget">The type of the target to deserialize to.</typeparam>
		/// <returns>
		/// A deserializer for the given target type.
		/// </returns>
		public static Mapper<TTarget> Create<TTarget>()
			where TTarget : new()
		{
			return new Mapper<TTarget>();
		}

		/// <summary>
		/// Creates a deserializer for the given target type.
		/// </summary>
		/// <param name="targetType">Type of the target to deserialize to.</param>
		/// <returns>A deserializer for the given target type.</returns>
		public static IMapper Create( Type targetType )
		{
			Contract.Requires( targetType != null );

			return (IMapper)Activator.CreateInstance( typeof( Mapper<> ).MakeGenericType( targetType ) );
		}
	}

	/// <summary>
	/// Interface for an dictionary mapper, where the target type is not known at compile time
	/// </summary>
	public interface IMapper
	{
		object MapDictionary( IDictionary<string, object> dictionary );
	}


	/// <summary>
	/// Mapper for dictionary data to the given target type
	/// </summary>
	/// <typeparam name="TTarget">The type of the target to deserialize to.</typeparam>
	public sealed class Mapper<TTarget>
		: IMapper
		where TTarget : new()
	{
		object IMapper.MapDictionary( IDictionary<string, object> dictionary )
		{
			return MapDictionary( dictionary );
		}

		public TTarget MapDictionary( IDictionary<string, object> dictionary )
		{
			// an empty dictionary means a null reference
			if( !dictionary.Any() )
				return default( TTarget );

			var targetInstance = new TTarget();

			// create copy of dictionary that we can alter
			// we remove every mapped property so we know if there are unmapped items left.
			var processingDictionary = new Dictionary<string, object>( dictionary );

			// get all public properties of the target type
			PropertyInfo[] properties = typeof( TTarget ).GetProperties( BindingFlags.Instance | BindingFlags.Public );
			foreach( PropertyInfo property in properties )
			{
				string name = property.Name;

				// check if there is a mapping
				var mapping = property.GetCustomAttribute<MappingAttribute>();

				// if there is mapping and the name is remapped, use the remapped name, not the actual property name
				if( mapping != null && !string.IsNullOrEmpty( mapping.Name ) )
					name = mapping.Name;

				bool needSet = true;

				if( mapping == null || !mapping.Ignore )
				{
					// try to get the value from the dictionary
					object value;

					// multiple names? get multiple values
					if( mapping != null && mapping.Names != null && mapping.Names.Any() )
					{
						var values = new List<object>();

						foreach( string aName in mapping.Names )
						{
							object aValue;
							if( !processingDictionary.TryGetValue( aName, out aValue ) )
							{
								if( !mapping.IsOptional )
									throw new MappingException( "The " + aName + " key is missing" );
								else
									aValue = null;

								processingDictionary.Remove( aName );
							}

							values.Add( aValue );
						}

						// when all values are null, there is no data because all optional fields where ommited.
						// so we can skip setting it, because conversion will yield no data, or maybe even an error
						if( values.All( t => t == null ) )
							needSet = false;

						value = values.ToArray();
					}
					// single name, get single value
					else if( !processingDictionary.TryGetValue( name, out value ) )
					{
						// when not found, we check if the property is optional
						if( mapping == null || ( mapping.IsOptional == false && mapping.Skip == false ) )
							throw new MappingException( "The " + name + " key is missing, but the " + property.Name + " property is not optional" );
					}

					if( value != null )
					{
						Type valueType = value.GetType();

						// if value in dictoinary is self a dictionary, start sub-mapping
						if( value is IDictionary<string, object> )
						{
							// create deserializer for target type
							IMapper nestedDeserializer = Mapper.Create( property.PropertyType );

							// deserialize and overwrite value with deserialized value
							value = nestedDeserializer.MapDictionary( (IDictionary<string, object>)value );
						}
						else if( !property.CanWrite )
						{
							// not writable, so it is REQUIRED to be a List or Array to populate
							// arrays map to single or multi dimensional arrays.
							// lists map to lists
							if( value.GetType().IsArray )
							{
								if( MapArray( targetInstance, property, value ) )
								{
									processingDictionary.Remove( name );
									needSet = false;
								}
							}
							else if( typeof( IList ).IsAssignableFrom( valueType ) )
							{
								if( MapList( targetInstance, property, value, valueType ) )
								{
									processingDictionary.Remove( name );
									needSet = false;
								}
							}
						}

						// found, so store in property
						if( needSet )
						{
							// booleans are stored as sbyte, so we have 'magic' mapping from sbyte to bool
							if( property.PropertyType == typeof( bool ) )
							{
								if( value is sbyte )
								{
									var sbyteValue = (sbyte)value;
									value = sbyteValue != 0;
								}
								// found also some locations where bools are stored as a string with the text true or false... sigh...
								else if( value is string )
								{
									var stringValue = (string)value;
									value = string.Equals( stringValue, "true", StringComparison.OrdinalIgnoreCase );
								}
								else
								{
									throw new MappingException( "Cannot map tool boolean. Only sbyte or string are supported." );
								}
							}

							try
							{
								property.SetValue( targetInstance, ConvertToTarget( property.PropertyType, value, mapping ) );
							}
							catch( InvalidCastException x )
							{
								throw new MappingException( 
									string.Format(
										"The source key {0} with value {1} ({2}), could not be mapped to the target property {3} ({4}).",
										name,
										value,
										value.GetType(),
										property.Name,
										property.PropertyType
									), 
									x
								);
							}
							processingDictionary.Remove( name );
						}
					}
				}
			}

			// if there are any mappings left, we  either:
			//  * Throw if target instance is not an expando object
			// * Add as expando property if it is an expando object
			if( processingDictionary.Count > 0 )
			{
				MapToExpando( targetInstance, processingDictionary );
			}

			return targetInstance;
		}

		private void MapToExpando( TTarget targetInstance, Dictionary<string, object> processingDictionary )
		{
			Contract.Requires<ArgumentNullException>( targetInstance != null );
			Contract.Requires<ArgumentNullException>( processingDictionary != null );

			var dynamic = targetInstance as DynamicObject;
			if( dynamic != null )
			{
				// dynamic supported, so add all the properties
				foreach( KeyValuePair<string, object> keyValue in processingDictionary )
				{
					var subDictionary = keyValue.Value as IDictionary<string, object>;
					if( subDictionary != null )
					{
						// sub dictionary creates it's own expando object
						// and will map to that expando object
						IMapper expandoMapper = Mapper.Create( typeof( ObservableExpandoObject ) );
						object expando = expandoMapper.MapDictionary( subDictionary );

						dynamic.TrySetMember( new MappingSetMemberBinder( keyValue.Key ), expando );
					}
					else
					{
						// al other values are simply added to this expando object
						dynamic.TrySetMember( new MappingSetMemberBinder( keyValue.Key ), keyValue.Value );
					}
				}
			}
			else
				throw new MappingException( "The key(s) " + String.Join( ", ", processingDictionary.Keys ) + " have no matching property on the target of type " + targetInstance.GetType().FullName );
		}

		private bool MapArray( TTarget targetInstance, PropertyInfo property, object value )
		{
			Contract.Requires<ArgumentNullException>( targetInstance != null );
			Contract.Requires<ArgumentNullException>( property != null );

			if( !property.PropertyType.IsArray )
				throw new MappingException( "When source is array, target should be array aswell" );

			// this is an array, so it should map to a single or multi dimensional array
			// lengths and item types should match
			var sourceArray = (Array)value;
			var destinationArray = (Array)property.GetValue( targetInstance );

			if( sourceArray.Rank != 1 )
				throw new MappingException( "For the source (dictionary) side, only single dimensional arrays are supported" );

			int destinationLength = 1;
			for( int i = 0; i < destinationArray.Rank; i++ )
				destinationLength *= destinationArray.GetLength( i );

			if( destinationLength != sourceArray.Length )
				throw new MappingException( "The array's on both sides need to match in length" );

			// now validate if item types are also the same
			Type sourceElemenType = sourceArray.GetType().GetElementType();
			Type destinationElementType = destinationArray.GetType().GetElementType();

			// if target type is enum, we support it, when underlying type of enum is same as source
			if( destinationElementType.IsEnum )
			{
				destinationElementType = Enum.GetUnderlyingType( destinationElementType );

				if( sourceElemenType != destinationElementType )
					throw new MappingException( "The array's on both sides need to match in type" );

				// cannot use Buffer.BlockCopy because enum is not primitive type according to it.
				CopyEnumArray( sourceArray, destinationArray );
			}
			else
			{
				if( !IsMappingMatchableType( sourceElemenType, destinationElementType ) )
					throw new MappingException( "The array's on both sides need to match in type (but not sign)." );

				// length and type validation done, now start 'mapping'
				// simply copy the data over as bytes
				Buffer.BlockCopy( sourceArray, 0, destinationArray, 0, destinationLength * Marshal.SizeOf( destinationElementType ) );
			}

			// no need to set the value, because we wrote to array 'in' property
			return true;
		}

		private bool MapList( TTarget targetInstance, PropertyInfo property, object value, Type valueType )
		{
			Contract.Requires<ArgumentNullException>( targetInstance != null );
			Contract.Requires<ArgumentNullException>( property != null );
			Contract.Requires<ArgumentNullException>( valueType != null );

			var sourceList = (IList)value;
			var targetList = (IList)property.GetValue( targetInstance, null );

			// it's a list, lets determine the type of the items in the list
			Type elementType = GetListElementType( valueType );

			// when it's a dictionary, we need to map the values themselfs to types
			if( typeof( IDictionary<string, object> ).IsAssignableFrom( elementType ) )
			{
				// get type of target
				Type targetElementType = GetListElementType( property.PropertyType );
				// create deserializer for target type
				IMapper listDeserializer = Mapper.Create( targetElementType );

				// loop over all
				targetList.Clear();
				foreach( IDictionary<string, object> compound in sourceList )
				{
					// convert
					object deserializedItem = listDeserializer.MapDictionary( compound );
					// and add to target list
					targetList.Add( deserializedItem );
				}

				return true;
			}

			return false;
		}

		private void CopyEnumArray( Array sourceArray, Array destinationEnumArray )
		{
			Contract.Requires( sourceArray != null );
			Contract.Requires( sourceArray.Rank == 1, "Only single dimensional source arrays are supported" );
			Contract.Requires( destinationEnumArray != null );

			Type targetType = destinationEnumArray.GetType().GetElementType();

			int simpleSourceIndex = 0;
			var indices = new int[destinationEnumArray.Rank];

			while( simpleSourceIndex < sourceArray.Length )
			{
				// convert and copy value
				destinationEnumArray.SetValue(
					Enum.ToObject(
						targetType,
						sourceArray.GetValue( simpleSourceIndex )
					),
					indices
				);

				// go to next value of source
				++simpleSourceIndex;

				// and go to next value of destination, which can be multi dimensional
				for( int j = indices.Length - 1; j >= 0; --j )
				{
					indices[j]++;

					// check if there is an overflow based on upper bound of this dimension
					if( indices[j] > destinationEnumArray.GetUpperBound( j ) )
						// overflow, reset this index, and increase 'next'
						indices[j] = 0;
					else
						// no overflow, done
						break;
				}
			}
		}

		private bool IsMappingMatchableType( Type left, Type right )
		{
			Contract.Requires( left != null );
			Contract.Requires( right != null );

			if( left == right )
				return true;

			// not same type, check if types only differ in sign (byte vs. sbyte, int vs uint, etc. )
			if( MakeNbtDefaultType( left ) == MakeNbtDefaultType( right ) )
				return true;

			return false;
		}

		/// <summary>
		/// Converts the type to a Nbt type. (add sign, to unsigned types)
		/// </summary>
		/// <param name="type">The type to convert.</param>
		/// <returns>The type in a Nbt usable style</returns>
		private Type MakeNbtDefaultType( Type type )
		{
			Contract.Requires( type != null );
			Contract.Ensures( Contract.Result<Type>() != null );

			// bool is stored as sbyte
			if( type == typeof( bool ) )
				return typeof( sbyte );

			// int's are stored signed
			if( type == typeof( byte ) )
				return typeof( sbyte );
			if( type == typeof( ushort ) )
				return typeof( short );
			if( type == typeof( uint ) )
				return typeof( int );
			if( type == typeof( ulong ) )
				return typeof( long );

			// keep type
			return type;
		}

		/// <summary>
		/// Tries to convert a Nbt value to a local value.
		/// </summary>
		/// <param name="targetType">Type of the target.</param>
		/// <param name="sourceValue">The source Nbt value.</param>
		/// <param name="mapping">The mapping.</param>
		/// <returns>
		/// The local value.
		/// </returns>
		/// <exception cref="MappingException">Underlying type of  + targetType.Name +  ( + underlyingEnumType.Name + ) does not match type of value  + sourceValue.ToString() +  ( + sourceValue.GetType().Name + )</exception>
		/// <exception cref="InvalidCastException">Thrown when the value could not be cast to the target type</exception>
		private object ConvertToTarget( Type targetType, object sourceValue, MappingAttribute mapping )
		{
			Contract.Requires( targetType != null );

			// if there is a convert, it should always be called, even if source and target are the same type
			if( mapping != null && mapping.Converter != null )
			{
				var converter = (IValueConverter)Activator.CreateInstance( mapping.Converter );
				return converter.FromNbtToLocal( sourceValue );
			}

			// test if we need to convert 
			if( targetType.IsAssignableFrom( sourceValue.GetType() ) )
				return sourceValue;

			// special mapping from byte to bool
			if( targetType == typeof( bool ) && ( sourceValue is sbyte || sourceValue is byte ) )
				return ( (byte)sourceValue ) != 0;

			// mapping of enums
			if( targetType.IsEnum )
			{
				Type underlyingEnumType = Enum.GetUnderlyingType( targetType );

				if( underlyingEnumType != sourceValue.GetType() )
					throw new MappingException( "Underlying type of " + targetType.Name + " (" + underlyingEnumType.Name + ") does not match type of value " + sourceValue + " (" + sourceValue.GetType().Name + ")" );

				return Enum.ToObject( targetType, sourceValue );
			}

			return Convert.ChangeType( sourceValue, targetType );
		}

		private Type GetListElementType( Type type )
		{
			Contract.Requires( type != null );
			Contract.Ensures( Contract.Result<Type>() != null );

			Type[] listTypes = (
				from interfaceType in type.GetInterfaces()
				where interfaceType.IsGenericType
				let genericInterfaceType = interfaceType.GetGenericTypeDefinition()
				where genericInterfaceType == typeof( IList<> )
				select interfaceType.GetGenericArguments().Single()
			).ToArray();

			if( listTypes.Length < 1 )
				throw new MappingException( type.FullName + " Implements no IList<> interface." );
			if( listTypes.Length > 1 )
				throw new MappingException( type.FullName + " Implements multiple IList<> interfaces. Unable to choose." );

			return listTypes[0];
		}

		/// <summary>
		/// Class to provide the SetMember bindings for the target ExpandoObject
		/// </summary>
		public sealed class MappingSetMemberBinder
			: SetMemberBinder
		{
			/// <summary>
			/// Initializes a new instance of the <see cref="MappingSetMemberBinder"/> class.
			/// </summary>
			/// <param name="name">The name.</param>
			public MappingSetMemberBinder( string name )
				: base( name, false )
			{
			}


			/// <summary>
			/// Performs the binding of the dynamic set member operation if the target dynamic object cannot bind.
			/// </summary>
			/// <returns>
			/// The <see cref="T:System.Dynamic.DynamicMetaObject"/> representing the result of the binding.
			/// </returns>
			/// <param name="target">The target of the dynamic set member operation.</param><param name="value">The value to set to the member.</param><param name="errorSuggestion">The binding result to use if binding fails, or null.</param>
			public override DynamicMetaObject FallbackSetMember( DynamicMetaObject target, DynamicMetaObject value, DynamicMetaObject errorSuggestion )
			{
				throw new NotImplementedException();
			}
		}

	}
}
